package core;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

import persistence.ContainerDTO;
import persistence.DataPersistor;
import persistence.ItemDTO;
import persistence.ProductDTO;

import common.util.DateUtils;

import core.exception.IllegalAddException;
import core.exception.IllegalEditException;
import core.exception.IllegalRemoveException;
import core.exception.NotFoundException;
import core.exception.SaveException;

/**
 *	ItemFacade is the interface between controllers and the ItemManager/Item section of the model
 *	This class is Observable, so it can add observers and notifies observers of changes in the 
 *	ItemManager
 *
 * @author Royce
 */

public class ItemFacade extends Observable {

	private static ItemFacade _instance;
	private IBarcodePrinter printer;
	private Observer _controller;
	
	/**
	 * A method to access the singleton Instance of this ItemFacade
	 * @return the instance, creating it if necessary.
	 * 
	 * @pre none.
	 * @post returns a valid ItemFacade.
	 */
	public static ItemFacade instance() {
		if (_instance == null) {
			_instance = new ItemFacade();
		}
		return _instance;
	}
	
	/**
	 * For testing purposes, remove all data that has to do with items.
	 */
	public static void clearAllData(){
		setInstance(null);
		ItemManager.setInstance(null);
	}
	
	/**
	 * For testing purposes.
	 * @param o
	 */
	public void addControllerForTest(Observer o){
		_controller = o;
	}
	
	/**
	 * Sets the instance of this ItemFacade to the one given
	 * @param facade the ItemFacade instance to replace this one
	 */
	protected static void setInstance(ItemFacade facade) {
		_instance = facade;
	}
	
	/**
	 * Private constructor.  This method should never be called outside the ItemFacade class.
	 */
	private ItemFacade() {
		//just make sure the instance exists
		ItemManager.instance();
		printer = new ITextPDFBarcodePrinter();
	}
	

	/**
	 * Determines whether it is valid to add a given Item into a given Container
	 * @param item The Item to test for adding.
	 * @param container The Container to test for adding into.
	 * @return true if it is valid to add the given Item into the given Container, 
	 * false otherwise.
	 * 
	 * @pre none. See Container.canAddItem for any side effects.
	 * @post see return statement.
	 */
	public boolean canAddItem(Item item, Container container) {
		return !(container.containsItem(item));
	}
	
	/**
	 * Adds the given Item into the given Container
	 * @param product The Product which is an instance of the Item to add
	 * @param container The Container to add the given Item into.
	 * @throws IllegalAddException if it is illegal to add the given Item 
	 * into the given Container.
	 * @return a reference to the added Item, if successful
	 * 
	 * @pre the item is nowhere in the system, container is not a RootStorageUnit.
	 * @post the item is added to the container.
	 * 
	 * See page 13 and Figure 2 on page 11
	 */
	public Item addItem(Product product, Container container) throws IllegalAddException, 
		SaveException {
		//Perform action
		Item item = ItemManager.instance().createItem(product, container);
		ItemManager.instance().addItemToContainer(container, item);
		
		//Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ItemDTO dto = new ItemDTO(item);
		DataPersistor.instance().getItemDAO().addItem(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		//Notify
		Notification notify = new Notification(item, NotificationType.ItemAdded);
		notifyObservers(notify);
		return item;
	}
	
	/**
	 * Add a list of already-created items to the ItemFacade. This method was created for undo/redo
	 * functionality
	 * @param items
	 */
	public void addItems(List<Item> items, Container container) throws SaveException{		
		
		ItemManager.instance().addItems(items);
		Iterator<Item> iter = items.iterator();
		while(iter.hasNext()){
			Item item = iter.next();
			ItemManager.instance().addItemToContainer(container, item);
			
			//Save
			DataPersistor.instance().getTransactionManager().beginTransaction();
			ItemDTO dto = new ItemDTO(item);
			DataPersistor.instance().getItemDAO().addItem(dto);
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		}
		Notification notify = new Notification(items, NotificationType.ItemsAdded);
		notifyObservers(notify);
	}
	
	/**
	 * Remove a list of items from the system.
	 * @param items The list of items to remove
	 * @throws IllegalRemoveException 
	 */
	public void removeItemsFromSystem(List<Item> items) throws IllegalRemoveException, 
		SaveException{
		Iterator<Item> item = items.iterator();
		while (item.hasNext()){
			Item tempItem = item.next();
			
			//Save is handled within this method
			this.removeItemFromSystem(tempItem);
		}
		
		Notification notify = new Notification(items, NotificationType.ItemsRemoved);
		notifyObservers(notify);			
	}
	
	/**
	 * Completely removes an item from the system.  This also removes the item from 
	 * the item history.
	 * @param item the Item to remove
	 * @throws IllegalRemoveException if the given Item is null or if it is illegal to 
	 * remove the Item for any reason.
	 */
	public void removeItemFromSystem(Item item) throws IllegalRemoveException,
		SaveException{
		if(item == null) throw new IllegalRemoveException("Null item");
		ItemManager.instance().removeItemFromSystem(item);
		
		//Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ItemDTO dto = new ItemDTO(item);
		DataPersistor.instance().getItemDAO().removeItem(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
	}
	
	/**
	 * Determines whether it is valid to remove the given Item from the system
	 * @param item The Item to test for removing
	 * @return true if it is valid to remove the given Item from the system, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement
	 */
	public boolean canRemoveItem(Item item) {
		return (ItemManager.instance().lookupByBarcode(item.getBarcode()) != null);
	}
	
	/**
	 * Removes the Item from the system using a string representation of the barcode
	 * 
	 * @param stringRep
	 * @throws IllegalRemoveException
	 * @throws NotFoundException
	 */
	
	public void removeItem(String stringRep) throws IllegalRemoveException, NotFoundException,
		SaveException {
		Barcode barcode = toBarcode(stringRep);
		Item tempItem = ItemManager.instance().lookupByBarcode(barcode);
		removeItem(tempItem);
	}
	
	/**
	 * Removes the given Item.
	 * @param item The Item to remove.
	 * @throws IllegalRemoveException if removing the given Item is invalid.
	 * @throws NotFoundException if the given Item was not found in the system.
	 * 
	 * @pre the item exists in the system
	 * @post the item has its exit date set and doesn't appear in the current items
	 * 
	 * See page 32 and Figure 6 on page 12
	 */
	public void removeItem(Item item) throws IllegalRemoveException, NotFoundException, 
		SaveException {
		Notification notify = new Notification(item, NotificationType.ItemRemoved);
		Item updated = ItemManager.instance().removeItem(item);
		
		//Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ItemDTO dto = new ItemDTO(updated);
		DataPersistor.instance().getItemDAO().updateItem(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		notifyObservers(notify);
	}
	
	/**
	 * For undo/redo, unRemove the given item
	 */
	public void unRemoveItem(Item item, Container container){
		ItemManager.instance().unRemoveItem(item,container);
		Notification notify = new Notification(item, NotificationType.ItemAdded);
		notifyObservers(notify);		
	}
	
	/**
	 * Determines if it is valid to move the given Item from where it is now 
	 * to the given Container.
	 * @param item The Item to test for moving.
	 * @param fromContainer The Container to move the item from.
	 * @param targetContainer The Container to test for moving into.
	 * @return true if it is valid to move the given Item to the given Container, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement.
	 */
	public boolean canMoveItem(Item item, Container fromContainer, Container targetContainer) {
		return item != null && fromContainer != null && 
				targetContainer != null && item.getContainer() == fromContainer;
		
	}
	
	/**
	 * Moves the given Item from where it is now to the given target Container.
	 * @param item The Item to move.
	 * @param fromContainer The Container to move the item from.
	 * @param targetContainer The Container to move the Item into.
	 * @throws IllegalAddException if it is illegal to move the given Item 
	 * into the target Container
	 * @throws NotFoundException if the given Item was not found in the from Container
	 * @deprecated use moveItem(Item, Container) instead
	 * 
	 * @pre the item exists but not in the targetContainer, fromContainer and targetContainer are
	 * not RootStorageUnit
	 * @post the item is now in targetContainer and not in fromContainer
	 */
	@Deprecated
	public void moveItem(Item item, Container fromContainer, Container targetContainer) 
			throws IllegalAddException, NotFoundException {
		Notification notify = new Notification(item, NotificationType.ItemAdded);
		ItemManager.instance().moveItem(targetContainer, item);
		notifyObservers(notify);
	}
	
	/**
	 * Moves the given Item from where it is now to the given target Container.
	 * @param item The Item to move.
	 * @param targetContainer The Container to move the Item into.
	 * @throws IllegalAddException if it is illegal to move the given Item 
	 * into the target Container
	 * 
	 * @pre the item exists but not in the targetContainer, fromContainer and targetContainer are
	 * not RootStorageUnit
	 * @post the item is now in targetContainer and not in fromContainer
	 */
	public void moveItem(Item item, Container targetContainer) throws IllegalAddException,
		SaveException {
		Notification notify = new Notification(item, NotificationType.ItemAdded);
		boolean addProduct = !targetContainer.containsProduct(item.getProduct());
		Container old = targetContainer.getStorageUnit().getContainerForProduct(item.getProduct());
		ItemManager.instance().moveItem(targetContainer, item);
		
		//Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ItemDTO dto = new ItemDTO(item);
		if (addProduct){
			ProductDTO pDto = new ProductDTO(item.getProduct());
			ContainerDTO cDTo = new ContainerDTO(targetContainer);
			DataPersistor.instance().getProductContainerDAO().addRelationship(pDto, cDTo);
		}
		if (old != targetContainer && old != null){
			ProductDTO pDto = new ProductDTO(item.getProduct());
			ContainerDTO cDTo = new ContainerDTO(old);
			DataPersistor.instance().getProductContainerDAO().removeRelationship(pDto, cDTo);
		
			Iterator<Item> movedItems = ItemManager.instance().itemsInProductAndContainer(
					targetContainer, item.getProduct());
			
			while(movedItems != null && movedItems.hasNext()){
				ItemDTO iDto = new ItemDTO(movedItems.next());
				DataPersistor.instance().getItemDAO().updateItem(iDto);
			}
		}
		DataPersistor.instance().getItemDAO().updateItem(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		notifyObservers(notify);
	}
	
	/**
	 * Return true if you can transfer an item. An item can be transferred if
	 * it isn't already in the target storage unit.
	 * 
	 * @param item The item
	 * @param container The target storage unit
	 */
	public boolean canTransferItem(Item i, Container c){
		if (!(c instanceof StorageUnit))
			return false;
		return !((StorageUnit)c).getDescendants().contains(i.getContainer());
	}
	
	/**
	 * Transfers a given Item from where it is now to the given target Container.
	 * see ItemManager.moveItem() and ItemManager.transferItem() for the differences between
	 * these two methods.
	 * @param item the Item to tranfer.
	 * @param targetContainer the Container to transfer the Item to.
	 * @throws IllegalAddException if it is illegal to transfer the Item.
	 */
	public void transferItem(Item item, Container targetContainer) throws IllegalAddException,
		SaveException {
		if (!canTransferItem(item, targetContainer)) {
			throw new IllegalAddException("That Item is already in the target container.");
		}
		
		boolean addProduct = false;
		if (targetContainer.getStorageUnit().getContainerForProduct(item.getProduct()) == null)
			addProduct = true;
		
		ItemManager.instance().transferItem(targetContainer, item);
		
		//Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ItemDTO dto = new ItemDTO(item);
		DataPersistor.instance().getItemDAO().updateItem(dto);
		if (addProduct){
			ProductDTO pDto = new ProductDTO(item.getProduct());
			ContainerDTO cDTo = new ContainerDTO(targetContainer);
			DataPersistor.instance().getProductContainerDAO().addRelationship(pDto, cDTo);			
		}
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		Notification notify = new Notification(item, NotificationType.ItemChanged);
		notifyObservers(notify);		
	}
	
	/**
	 * Determines if it is valid to edit a given Item
	 * @param item the Item to test for editing
	 * @param newDate the new Date to test for editing.
	 * @return true if it is valid to edit the given Item, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement
	 */
	public boolean canEditItem(Item item, String newDate) {
		return Item.isValidDate(newDate);
	}
	
	/**
	 * Edits the given Item
	 * @param before The Item as it exists now
	 * @param newDate a String that specifies the new Date
	 * @throws IllegalEditException if it is invalid to edit the given Item.
	 * @throws NotFoundException if the given Item is not found in the system to edit.
	 * 
	 * @pre before exists in the system, newDate is a valid date
	 * @post before now has the new date
	 */
	public void editItem(Item before, String newDate) 
			throws IllegalEditException, NotFoundException, SaveException {
		before.changeEntryDate(newDate);
		Item newItem = ItemManager.instance().lookupByBarcode(before.getBarcode());
		
		//Save
		DataPersistor.instance().getTransactionManager().beginTransaction();
		ItemDTO dto = new ItemDTO(newItem);
		DataPersistor.instance().getItemDAO().updateItem(dto);
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		
		Notification notify = new Notification(newItem, NotificationType.ItemChanged);
		notifyObservers(notify);
	}
	
	/**
	 * Inserts multiple instances of a given Product into the system
	 * @param product The Product that the created items will be instances of
	 * @param count The number of items to add.
	 * @param entryDate The given entry Date.  If entryDate is null, then the current time
	 * will be used for this value.
	 * @throws IllegalAddException If it is illegal to add the given item.
	 * @return The added items.
	 * 
	 * @pre count > 0, product != null and exists within the system
	 * @post count number of items are in the system
	 */
	public List<Item> createItems(Product product, Container container, 
			int count, String entryDate) throws IllegalAddException, SaveException {
		boolean customDate = true;
		
		if (product == null) throw new IllegalAddException("Null product");
		if (entryDate == null || entryDate.isEmpty() || 
				entryDate.equals(DateUtils.formatDate(DateUtils.currentDate()))) 
			customDate = false;
		
		boolean addedProduct = !container.containsProduct(product);
		
		List<Item> itemsAdded = new ArrayList<Item>();
		DataPersistor.instance().getTransactionManager().beginTransaction();
		for (int i = 0; i < count; i++) {
			Item item = ItemManager.instance().createItem(product, container);
			ItemManager.instance().addItemToContainer(container, item);
			if(customDate) item.changeEntryDate(entryDate);
			itemsAdded.add(item);
			
			//Save
			ItemDTO dto = new ItemDTO(item);
			DataPersistor.instance().getItemDAO().addItem(dto);
			
		}
		
		if (addedProduct){
			ProductDTO pDTO = new ProductDTO(product);
			ContainerDTO cDTO = new ContainerDTO(container);
			DataPersistor.instance().getProductContainerDAO().addRelationship(pDTO, cDTO);
		}
		
		DataPersistor.instance().getTransactionManager().endTransaction(true);
		Notification notify = new Notification(itemsAdded, NotificationType.ItemsAdded);
		notifyObservers(notify);
		return itemsAdded;
	}
	
	/**
	 * Generates a PDF file containing the bar codes of the given Items
	 * @param items The Items to generate a report for.
	 * @param filename The location to store the PDF report.  If this location String is null
	 * or empty, a generic filename using a time/date stamp will be used.
	 * @throws IOException if an error occurred opening, creating, or writing to the file
	 */
	public void printBarcodes(List<Item> items, String filename) throws IOException {
		printer.printBarcodes(items);
	}
	
	/**
	 * Returns an iterator over all the Items in the system whose Products have the given
	 * barcode as given as a String representation.
	 * @param barcode a String representation of the Product to be used in finding Items.
	 * @return an Iterator over all Items found
	 * @throws NotFoundException if there are no Items for the given Product barcode.
	 */
	public Iterator<Item> getItemsInProduct(String barcode) throws NotFoundException {
		Barcode pBarcode = new Barcode(barcode, BarcodeType.PRODUCT);
		Product product = ProductManager.instance().getProduct(pBarcode);
		Iterator<Item> items = ItemManager.instance().getItemsInProduct(product);
		if(items == null) throw new NotFoundException("Product " + barcode + " not found.");
		if(!items.hasNext())throw new NotFoundException("Product " + barcode + " not found.");
		return items;
	}
	
	/**
	 * Looks up an Item in the system given a String representation of a Barcode
	 * @param barcodeRep the String representation of a barcode
	 * @return the reference to the Item
	 * @throws NotFoundException if the Item was not found
	 * 
	 * @pre none.
	 * @post see return statement and throws statement
	 */
	public Item getItem(String barcodeRep) throws NotFoundException {
		if (!Barcode.isValidItemBarcode(barcodeRep)) {
			throw new NotFoundException(barcodeRep + " is an invalid barcode.");
		}
		Barcode barcode = toBarcode(barcodeRep);
		Item item = ItemManager.instance().lookupByBarcode(barcode);
		if (item == null) throw new NotFoundException("Item " + barcodeRep + " not found.");
		return item;
	}
	
	/**
	 * Queries the system to determine if there is an Item with the given barcode
	 * @param barcode a String representation of the barcode to search for
	 * @return true if the Item exists, false otherwise.
	 */
	public boolean containsItem(String barcode){
		if (!Barcode.isValidItemBarcode(barcode)) return false;
		Barcode tempBarcode = toBarcode(barcode);
		return ItemManager.instance().lookupByBarcode(tempBarcode) != null;
	}

	/**
	 * 
	 * @param barcode a String representation of a barcode
	 * @return an Item type Barcode object.
	 */
	protected Barcode toBarcode(String barcode) {
		Barcode retVal = new Barcode(barcode.substring(0, barcode.length()-1), BarcodeType.ITEM);
		return retVal;
	}
	
	/**
	 * 
	 * @return a reference to this Facade's Manager, used for testing purposes only.
	 */
	protected ItemManager get_itemManager() {
		return ItemManager.instance();
	}
	
	@Override
	public void notifyObservers(Object arg) {
		if (_controller == null){
			this.setChanged();
			super.notifyObservers(arg);
		}
		else
			_controller.update(this, arg);
	}

	/**
	 * Gets all the items for a given product and container
	 * 
	 * @param product the product
	 * @param container the container
	 * 
	 * @return an iterator for the items
	 */
	public Iterator<Item> getItemsInProductAndContainer(Product product, Container container) {
		return ItemManager.instance().itemsInProductAndContainer(container, product);
	}
	
	/**
	 * Return the number of items for a given product and container.
	 * 
	 * @param product the product
	 * @param container the container
	 * 
	 * @return an iterator for the items
	 */
	public int getNumItemsInProductAndContainer(Product product, Container container){
		return ItemManager.instance().numItemsInProductAndContainer(container, product);
	}
	
	/**
	 * 
	 * @return a reference to all items which have not been deleted
	 */
	public Set<Item> getAllItems() {
		Iterator<Item> iter = ItemManager.instance().getAllItems();
		Set<Item> set = new HashSet<Item>();
		while (iter.hasNext())
		{
			set.add(iter.next());
		}
		return set;
	}

	/**
	 * Counts the number of Items that exists in the system given a Product barcode
	 * @param barcode a String representation of a Product barcode
	 * @return the number of Items whose products match the given barcode
	 */
	public int getNumberOfItemsInProduct(String barcode) 
	{
		try 
		{
			Iterator<Item> iter = getItemsInProduct(barcode);
			int count = 0;
			
			if (iter != null)
			{
				while (iter.hasNext())
				{
					iter.next();
					count++;
				}
			}
			return count;
		} 
		catch (NotFoundException e) 
		{
			//no items for that product
			return 0;
		}
	}
	
	public void decrementBarcode() {
		ItemManager.instance().decrementBarcode();
	}
}
